{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# News recommendation serving\n",
    "We will now develop our news recommendation training example into a simple news reader application. We will use an existing web-frontend https://github.com/saqueib/qreader and develop a flask based backend for it, which uses Ray actors to serve news and our news recommendation model.\n",
    "\n",
    "### Implementing the backend with a Ray actor\n",
    "\n",
    "<html><img src=\"newsreader_4.png\"/></html>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import absolute_import\n",
    "from __future__ import division\n",
    "from __future__ import print_function\n",
    "\n",
    "import atoma\n",
    "from flask import Flask, jsonify, request\n",
    "from flask_cors import CORS\n",
    "import os\n",
    "import requests\n",
    "import ray"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ray.init(num_cpus=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We define an actor `NewsServer`, which is responsible for parsing an RSS feed and extracting the news items so they can be sent to the frontend. It also has a method `like_item`, which is called whenever the user \"likes\" and article. Note that this is a toy example, but in a more realistic applications, we could have a number of these actors, for example one for each user, to distribute the load."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@ray.remote\n",
    "class NewsServer(object):\n",
    "\n",
    "    def __init__(self):\n",
    "        pass\n",
    "\n",
    "    def retrieve_feed(self, url):\n",
    "        response = requests.get(url)\n",
    "        # Sometimes there are parsing errors \n",
    "        feed = atoma.parse_rss_bytes(response.content)\n",
    "        items = []\n",
    "        for item in feed.items:\n",
    "            color = \"#FFFFFF\" # white\n",
    "            items.append({\"title\": item.title,\n",
    "                          \"link\": item.link,\n",
    "                          \"description\": item.description,\n",
    "                          \"description_text\": item.description,\n",
    "                          \"pubDate\": str(item.pub_date),\n",
    "                          \"color\": color})\n",
    "\n",
    "        return {\"channel\": {\"title\": feed.title,\n",
    "                            \"link\": feed.link,\n",
    "                            \"url\": feed.link},\n",
    "                \"items\": items}\n",
    "\n",
    "    def like_item(self, url, is_faved):\n",
    "        if is_faved:\n",
    "            print(\"url {} has been favorited\".format(url))\n",
    "        else:\n",
    "            print(\"url {} has been defavorited\".format(url))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Deploying the backend\n",
    "\n",
    "The following cell will set up a flask webserver that listens to commands from the frontend and dispatches them to the `NewsServer` actor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "app = Flask(\"newsreader\")\n",
    "CORS(app)\n",
    "\n",
    "@app.route(\"/api\", methods=[\"POST\"])\n",
    "def dispatcher():\n",
    "    req = request.get_json()\n",
    "    method_name = req[\"method_name\"]\n",
    "    method_args = req[\"method_args\"]\n",
    "    if hasattr(dispatcher.server, method_name):\n",
    "        method = getattr(dispatcher.server, method_name)\n",
    "        # Doing a blocking ray.get right after submitting the task\n",
    "        # might be bad for performance if the task is expensive.\n",
    "        result = ray.get(method.remote(*method_args))\n",
    "        return jsonify(result)\n",
    "    else:\n",
    "        return jsonify(\n",
    "            {\"error\": \"method_name '\" + method_name + \"' not found\"})\n",
    "\n",
    "dispatcher.server = NewsServer.remote()\n",
    "app.run(host=\"0.0.0.0\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To try out the backend, go to http://localhost:9000/. You can then click on the \"Add Channel\" button and enter the URL of a newsfeed, for example `http://news.ycombinator.com/rss`. Click on one of the star icons and observe how the information is propagated to the Ray actor (it will be printed in the above cell)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Serving the model\n",
    "\n",
    "<html><img src=\"newsreader_5.png\"/></html>\n",
    "\n",
    "We can now integrate the model we have trained in the `news_recommendation_training` example. **First you need to click Kernel/Restart & Clear Output** to prepare restarting the flask server. Then we import the needed modules and start Ray:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import absolute_import\n",
    "from __future__ import division\n",
    "from __future__ import print_function\n",
    "\n",
    "import atoma\n",
    "from flask import Flask, jsonify, request\n",
    "from flask_cors import CORS\n",
    "import os\n",
    "import requests\n",
    "import ray"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ray.init(num_cpus=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you already completed this example, put the name of the file that generated the best model below and evaluate the following cell:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "best_result_path = \"\"\n",
    "import pickle\n",
    "with open(best_result_path, \"rb\") as f:\n",
    "    pipeline = pickle.load(f)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE**: Copy the `NewsServer` code from above into the cell below and change it so that `color` will be\n",
    "\n",
    "```python\n",
    "color = \"#FFD700\" # gold-ish yellow\n",
    "```\n",
    "\n",
    "if `pipeline.predict([item.title])` is `True` and\n",
    "\n",
    "```python\n",
    "color = \"#FFFFFF\" # white\n",
    "```\n",
    "\n",
    "otherwise."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The new code for the NewsServer goes here"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can restart the flask server:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "app = Flask(\"newsreader\")\n",
    "CORS(app)\n",
    "\n",
    "@app.route(\"/api\", methods=[\"POST\"])\n",
    "def dispatcher():\n",
    "    req = request.get_json()\n",
    "    method_name = req[\"method_name\"]\n",
    "    method_args = req[\"method_args\"]\n",
    "    if hasattr(dispatcher.server, method_name):\n",
    "        method = getattr(dispatcher.server, method_name)\n",
    "        # Doing a blocking ray.get right after submitting the task\n",
    "        # might be bad for performance if the task is expensive.\n",
    "        result = ray.get(method.remote(*method_args))\n",
    "        return jsonify(result)\n",
    "    else:\n",
    "        return jsonify(\n",
    "            {\"error\": \"method_name '\" + method_name + \"' not found\"})\n",
    "\n",
    "dispatcher.server = NewsServer.remote()\n",
    "app.run(host=\"0.0.0.0\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To try out the backend with model serving integrated, go to http://localhost:9000/. You can again click on the \"Add Channel\" button and enter the URL of a newsfeed, for example http://news.ycombinator.com/rss. It will show recommended articles in yellow."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
